package de.dnb.gnd.utils;

import static de.dnb.gnd.utils.RecordUtils.getLines;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.naming.OperationNotSupportedException;

import de.dnb.basics.Constants;
import de.dnb.basics.applicationComponents.StringUtils;
import de.dnb.basics.applicationComponents.Pair;
import de.dnb.basics.applicationComponents.RangeCheckUtils;
import de.dnb.basics.applicationComponents.StreamUtils;
import de.dnb.basics.filtering.FilterUtils;
import de.dnb.basics.filtering.IFunction;
import de.dnb.basics.filtering.IPredicate;
import de.dnb.gnd.exceptions.IllFormattedLineException;
import de.dnb.gnd.exceptions.WrappingHandler;
import de.dnb.gnd.parser.Field;
import de.dnb.gnd.parser.Format;
import de.dnb.gnd.parser.Indicator;
import de.dnb.gnd.parser.Record;
import de.dnb.gnd.parser.RecordParser;
import de.dnb.gnd.parser.RecordReader;
import de.dnb.gnd.parser.Subfield;
import de.dnb.gnd.parser.line.Line;
import de.dnb.gnd.parser.line.LineFactory;
import de.dnb.gnd.parser.line.LineParser;
import de.dnb.gnd.parser.tag.GNDTag;
import de.dnb.gnd.parser.tag.GNDTagDB;
import de.dnb.gnd.parser.tag.Tag;
import de.dnb.gnd.parser.tag.TagDB;

/**
 * Enthält spezielle Hilfsfunktionen.
 * 
 * @author baumann
 *
 */
public final class GNDUtils {

    public static final String explicitSubfieldSep = "\n\t$";

    public static final String alephSubfieldSep = "$$";

    public static final String alephTagSubfieldSep = "   L ";

    public static final String explicitIndicatorContentSep = "  ";

    public static final String alephIndicatorContentSep = "";

    private static final GNDTagDB TAG_DB = GNDTagDB.getDB();

    private GNDUtils() {
        super();
    }

    /**
     * Sichert zu, dass record nicht null ist und mithilfe der
     * GND-Tag-Datenbank aufgebaut worden ist (also kein
     * Titeldatensatz ist). Andernfalls wird eine 
     * {@link IllegalArgumentException} geworfen.
     * 
     * 
     * @param record	nicht null
     */
    public static void assertGNDRecord(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        if (!RecordUtils.isAuthority(record)) {
            throw new IllegalArgumentException("Datensatz mit idn "
                + record.getId() + "\n" + record.getRawData()
                + "\nist kein Normdatensatz");
        }
    }

    /**
     * Liefert die Hauptansetzung.
     * 
     * @param record	nicht null.
     * @return			Hauptansetzung 
     * @throws IllegalStateException
     * 					wenn Zahl der 1xx != 1
     */
    public static Line getHeading(final Record record) {
        assertGNDRecord(record);
        List<Line> lines = getLines1XX(record);
        if (lines.size() == 1)
            return lines.get(0);
        else
            throw new IllegalStateException("Anzahl der 1XX ungleich 1");
    }

    /**
     * Liefert die 1xx-Felder.
     * 
     * @param record	nicht null.
     * @return 			nicht null.
     */
    public static ArrayList<Line> getLines1XX(final Record record) {
        assertGNDRecord(record);
        Collection<Tag> gNDTags = TAG_DB.getTag1XX();
        return getLines(record, gNDTags);
    }

    /**
     * Liefert die 4xx-Felder.
     * 
     * @param record	nicht null.
     * @return 			nicht null.
     */
    public static ArrayList<Line> getLines4XX(final Record record) {
        assertGNDRecord(record);
        Collection<Tag> gNDTags = TAG_DB.getTag4XX();
        return getLines(record, gNDTags);
    }

    /**
     * Liefert alle 5xx-Felder.
     * 
     * @param record	nicht null.
     * @return 			nicht null.
     */
    public static ArrayList<Line> getLines5XX(final Record record) {
        assertGNDRecord(record);
        Collection<Tag> gNDTags = GNDTagDB.getDB().getTag5XX();
        return getLines(record, gNDTags);
    }

    /**
     * Liefert die 7xx-Felder.
     * 
     * @param record	nicht null.
     * @return 			nicht null.
     */
    public static ArrayList<Line> getLines7XX(final Record record) {
        assertGNDRecord(record);
        Collection<Tag> gNDTags = GNDTagDB.getDB().getTag7XX();
        return getLines(record, gNDTags);
    }

    /**
     * Liefert die 5xx-Felder, außer 548, da das nicht relationiert ist.
     * 
     * @param record	nicht null.
     * @return 			nicht null.
     */
    public static ArrayList<Line> getRelatedLines5XX(final Record record) {
        assertGNDRecord(record);
        Collection<Tag> gNDTags = GNDTagDB.getDB().getRelatedTag5XX();
        return getLines(record, gNDTags);
    }

    /**
     * Gibt alle Zeilen, deren $4 mit prefix anfängt.
     * @param lines		nicht null
     * @param prefix	nicht null
     * @return			nicht null
     */
    public static List<Line> getLinesWithDollar4(
        final List<Line> lines,
        final String prefix) {
        Dollar4Predicate predicate = new Dollar4Predicate(prefix);
        return RecordUtils.filter(lines, predicate);
    }

    /**
     * Gibt alle Oberbegriffe, also alle 5XX-Zeilen, deren $4 "ob.."  oder 
     * "adue" lautet.
     * 
     * @param record	nicht null
     * @return			nicht null
     */
    public static List<Line> getOBB(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<Line> obb = getRelatedLines5XX(record);
        List<Line> linesOB = getLinesWithDollar4(obb, "ob");
        linesOB.addAll(getLinesWithDollar4(obb, "adue"));
        return linesOB;
    }

    /**
     * Gibt die alte(n) Normdatei(en) eines Datensatzes (z.Z. swd, gkd, pnd,
     * est).
     * 
     * @param record	nicht null.
     * @return			Liste, eventuell leer.
     */
    public static List<String> getOriginalAuthorityFile(final Record record) {
        assertGNDRecord(record);
        ArrayList<Line> lines = getOriginalHeadingLines(record);
        return SubfieldUtils.getContentsOfFirstSubfields(lines, 'S');
    }

    /**
     * Gibt die alte(n) Ansetzungen eines Datensatzes.
     * 
     * @param record	nicht null.
     * @return			Liste, eventuell leer.
     */
    public static List<String> getOriginalHeadings(final Record record) {
        assertGNDRecord(record);
        ArrayList<Line> lines = getOriginalHeadingLines(record);
        return SubfieldUtils.getContentsOfFirstSubfields(lines, 'a');
    }

    /**
     * Gibt die 913-Zeilen, die die alten Ansetzungen (nebst anderem) 
     * enthalten.
     * 
     * @param record	nicht null.
     * @return			Liste, eventuell leer.
     */
    public static ArrayList<Line> getOriginalHeadingLines(final Record record) {
        assertGNDRecord(record);
        Field field913 = RecordUtils.getFieldGivenAsString(record, "913");
        if (field913 != null)
            return new ArrayList<Line>(field913.getLines());
        else
            return new ArrayList<Line>();
    }

    /**
     * Gibt die 083-Zeilen, die die gültigen DDC-Nummern enthalten.
     * 
     * @param record	nicht null.
     * @return			Liste, eventuell leer.
     */
    public static ArrayList<Line> getValidDDCLines(final Record record) {
        assertGNDRecord(record);
        Field field083 = RecordUtils.getFieldGivenAsString(record, "083");
        if (field083 != null)
            return new ArrayList<Line>(field083.getLines());
        else
            return new ArrayList<Line>();
    }

    /**
     * Gibt die 089-Zeilen, die die veralteten DDC-Nummern enthalten.
     * 
     * @param record    nicht null.
     * @return          Liste, eventuell leer.
     */
    public static ArrayList<Line> getDeprecatedDDCLines(final Record record) {
        assertGNDRecord(record);
        Field field089 = RecordUtils.getFieldGivenAsString(record, "089");
        if (field089 != null)
            return new ArrayList<Line>(field089.getLines());
        else
            return new ArrayList<Line>();
    }

    /**
     * Gibt alle DDC-Nummern (gültig oder ungültig).
     * 
     * @param record    nicht null.
     * @return          Liste, eventuell leer.
     */
    public static ArrayList<Line> getAllDDCLines(final Record record) {
        assertGNDRecord(record);
        ArrayList<Line> lines = getValidDDCLines(record);
        lines.addAll(getDeprecatedDDCLines(record));
        return lines;
    }

    /**
     * Gibt die gültigen DDC-Nummern eines GND-Datensatzes.
     * 
     * @param record	nicht null.
     * @return			Liste, eventuell leer.
     */
    public static List<String> getValidDDCNumbers(final Record record) {
        assertGNDRecord(record);
        ArrayList<Line> lines = getValidDDCLines(record);
        return SubfieldUtils.getContentsOfFirstSubfields(lines, 'c');
    }

    /**
     * Gibt die DDC-Nummer einer DDC-Zeile.
     * 
     * @param line	nicht null.
     * @return		DDC-Nummer oder null
     */
    public static String getDDCNumber(final Line line) {
        return SubfieldUtils.getContentOfFirstSubfield(line, 'c');
    }

    /**
     * Gibt die Determiniertheit einer DDC-Zeile.
     * 
     * @param line	nicht null.
     * @return		DDC-Determiniertheit oder null
     */
    public static String getDDCDeterminacy(final Line line) {
        return SubfieldUtils.getContentOfFirstSubfield(line, 'd');
    }

    /**
     * Gibt zu einem Datensatz alle (gültigen und ungültigen) Paare
     * (DDC, Determiniertheit).
     * 
     * @param record    nicht null    
     * @return          Liste von Paaren, nicht null, eventuell leer. 
     *                  Die Paare (DDC, Determiniertheit) enthalten eventuell
     *                  an einer Stelle eine null
     */
    public static List<Pair<String, String>> getAllDDCNumbersAndDet(
        final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<Pair<String, String>>pairs = new LinkedList<>(); 
        ArrayList<Line> ddcs = getAllDDCLines(record);
        for (Line line : ddcs) {
            String ddc = getDDCNumber(line);
            String det = getDDCDeterminacy(line);
            Pair<String, String> pair = new Pair<String, String>(ddc, det);
            pairs.add(pair);
        }
        return pairs;

    }

    /**
     * splitted Feldinhalt in Information + Kommentar.
     * @param line	nicht null.
     * @return	Paar aus 
     * a) Information(en) und 
     * b) Kommentar(en) als Strings. 
     */
    public static Pair<List<Subfield>, List<String>> splitCommentAsString(
        final Line line) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        Pair<List<Subfield>, List<Subfield>> pair = splitCommentsFrom(line);
        IFunction<Subfield, String> function =
            SubfieldUtils.FUNCTION_SUBFIELD_TO_CONTENT;
        return new Pair<List<Subfield>, List<String>>(pair.first,
            FilterUtils.map(pair.second, function));
    }

    /**
     * splitted Feldinhalt in Information + Kommentar.
     * @param line	nicht null.
     * @return	Paar aus 
     * 				a) Information(en) und 
     * 				b) Kommentar(en). 
     */
    public static Pair<List<Subfield>, List<Subfield>> splitCommentsFrom(
        final Line line) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        List<Subfield> subfields = line.getSubfields();
        IPredicate<Subfield> predicate = new IPredicate<Subfield>() {
            @Override
            public boolean accept(final Subfield subfield) {
                return subfield.getIndicator().indicatorChar != 'v';
            }
        };
        return FilterUtils.divide(subfields, predicate);
    }

    /**
     * Liefert den ersten Kommentar.
     * @param line	nicht null
     * @return		ersten Kommentar oder null.
     */
    public static String getFirstComment(final Line line) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        Subfield dollarv = SubfieldUtils.getFirstSubfield(line, 'v');
        String comment = (dollarv == null) ? null : dollarv.getContent();
        return comment;
    }

    /**
     * Entfernt Kommentare aus Zeile und liefert Liste der restlichen
     * Unterfelder.
     * 
     * @param line 	nicht null
     * @return		nicht null, eventuell leer.
     */
    public static List<Subfield> removeComments(final Line line) {
        return SubfieldUtils.removeSubfields(line, 'v');
    }

    /**
     * Liefert
     * Pos. 2: Satztyp.
     * 	"p" – Person (individualisiert) 
     * 	"n" – Personenname (nicht indiv.) 
     * 	"b" - Körperschaft 
     * 	"f" - Kongress 
     * 	"u" - Werk 
     * 	"g" - Geografikum 
     * 	"s" - Sachbegriff 	 
     * @param record	nicht null
     * @return			Satztyp oder 0, wenn nicht enthalten.
     */
    public static char getRecordType(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getDatatypeCharacterAt(record, 1);
    }

    /**
     * Gibt Pos.3: Katalogisierungslevel von Feld 005.
     * @param record	nicht null
     * @return			Level oder +48, wenn nicht enthalten
     */
    public static int getLevel(final Record record) {
        int level = (RecordUtils.getDatatypeCharacterAt(record, 2)) - ('0');
        // wenn getDatatype ... den Wert 0 liefert, wird level -'0' = -48:
        if (level < 0)
            level = -level;
        return (RecordUtils.getDatatypeCharacterAt(record, 2)) - ('0');
    }

    /**
     * Ist das ein Hinweissatz?
     * Pos.4: Code für Hinweissatz: „e“ 
     * 
     * @param record	nicht null
     * @return			true, wenn ein Hinweissatz.
     */
    public static boolean isUseCombination(final Record record) {
        return (RecordUtils.getDatatypeCharacterAt(record, 3) == 'e');
    }

    /**
     * Ist der GND-Datensatz gültig?
     * 
     * @param record	nicht null 
     * @return			Datensatz enthält keine 010 (Änderungscodierung).
     * 					Nicht mehr gültige Datensätze enthalten eine 010.
     */
    public static boolean isValid(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        return !RecordUtils.containsField(record, "010");
    }

    /**
     * Gibt die Entitätencodierung(en).
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getEntityTypes(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfSubfields(record, "008");
    }

    public static boolean containsEntityTypes(final Record record) {
        assertGNDRecord(record);
        return !getEntityTypes(record).isEmpty();
    }

    /**
     * Gibt die GND-URI(s).
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getURIs(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfSubfields(record, "006");
    }

    /**
     * Liefert zu einem Tag den Grundtag (z.B. 500 -> 100).
     * @param tag	nicht null
     * @return		auch null, wenn nichts vorhanden
     */
    public static Tag getNamingRelevantTag(final Tag tag) {
        RangeCheckUtils.assertReferenceParamNotNull("tag", tag);
        StringBuffer buffer = new StringBuffer(tag.pica3);
        buffer.setCharAt(0, '1');
        return TAG_DB.findTag(buffer.toString());
    }

    /**
     * Extrahiert aus $8 oder aus den vorhandenen Unterfeldern
     * die Ansetzungsform des relationierten Datensatzes, die
     * in der $8 angezeigt würde.
     * 
     * @param line	nicht null
     * @return		nicht null, eventuell leer
     */
    public static String getNameOfRelatedRecord(Line line) {
        // erster Versuch:
        String name = SubfieldUtils.getContentOfFirstSubfield(line, '8');
        if (name == null) {
            // Vielleicht ist die Quelle der Datenabzug:
            List<Subfield> subfields =
                SubfieldUtils.getNamingRelevantSubfields(line);
            Tag tag = getNamingRelevantTag(line.getTag());
            if (tag == null)
                throw new IllegalArgumentException(
                    "Zeile hat kein korrespondierendes 1XX");
            name = RecordUtils.toPicaWithoutTag(tag, subfields);
        }
        return name;
    }

    /**
     * Gibt den Namen des Datensatzes, wie er in $8 angezeigt würde.
     * Wirft eine {@link IllegalStateException}, wenn z.B. mehrere 1XX-Zeilen.
     * 
     * @param record	nicht null
     * @return			nicht null
     */
    public static String getNameOfRecord(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String name;
        if (WorkUtils.isWork(record))
            name = WorkUtils.getExpansionTitle(record);
        else {
            Line line1XX = getHeading(record);
            name =
                RecordUtils.toPicaWithoutTag(line1XX, Format.PICA3, true, '$');
        }
        // Führendes $ entfernen:
        if (name.startsWith("" + Constants.DOLLAR)
            || name.startsWith("" + Constants.FLORIN))
            name = name.substring(2);
        return name;
    }

    /**
     * Gibt die Teilbestandskennzeichen.
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getTbs(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfSubfields(record, "011");
    }

    final static IPredicate<Line> predicate = new IPredicate<Line>() {
        @Override
        public boolean accept(final Line line) {
            if (SubfieldUtils.containsIndicator(line, '4')) {
                Subfield subfield4 = SubfieldUtils.getFirstSubfield(line, '4');
                String subCont = subfield4.getContent();
                return subCont.equals("datl");
            } else
                return false;
        }
    };

    /**
     * Liefert Zeile mit Lebensdaten (548 ... $4datl).
     * 
     * @param record	nicht null
     * @return			null, wenn keine vorhanden, die Zeile, wenn genau
     * 					eine vorhanden, wirft eine IllegalStateException,
     * 					wenn mehrere vorhanden			
     */
    public static Line getDatlLine(Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        ArrayList<Line> lines548 =
            RecordUtils.getLines(record, predicate, GNDTagDB.TAG_548);
        if (lines548.size() == 1)
            return lines548.get(0);
        else if (lines548.isEmpty())
            return null;
        else
            throw new IllegalStateException("Datensatz " + record.getId()
                + " hat mehrere Lebensdaten");
    }

    /**
     * Liefert die Zeile mit der GND-Systematik.
     * 
     * @param record 	nicht null.
     * @return			Zeile mit Systematik oder null.
     */
    public static Line getGNDClassificationLine(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getTheOnlyLine(record, "065");
    }

    public static boolean containsGNDClassification(final Record record) {
        assertGNDRecord(record);
        return getGNDClassificationLine(record) != null;
    }

    /**
     * Gibt die GND-Systematik-Nummern.
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getGNDClassifications(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfSubfields(record, "065");
    }

    /**
     * Enthält der Datensatz die Systematiknummer?
     * @param record			nicht null.
     * @param classification	beleibig.
     * @return					true, wenn eine der Systematiknummern mit
     * 							classification übereinstimmt.
     */
    public static boolean containsGNDClassification(
        final Record record,
        String classification) {
        assertGNDRecord(record);
        List<String> gndClasses = getGNDClassifications(record);
        return gndClasses.contains(classification);
    }

    /**
     * Enthält der Datensatz eine der Systematiknummern?
     * @param record            nicht null.
     * @param classifications   beliebig.
     * @return                  true, wenn eine der Systematiknummern des
     *                          Datensatzes mit einer der
     *                          classifications übereinstimmt.
     */
    public static boolean containsGNDClassifications(
        final Record record,
        final String... classifications) {
        assertGNDRecord(record);
        for (String classification : classifications) {
            if (containsGNDClassification(record, classification))
                return true;
        }
        return false;
    }

    /**
     * Enthält der Datensatz die Systematiknummer trunkiert?
     * @param record			nicht null.
     * @param classification	beleibig.
     * @return					true, wenn eine der Systematiknummern mit
     * 							classification beginnt.
     */
    public static boolean containsGNDClassificationTrunk(
        final Record record,
        String classification) {
        assertGNDRecord(record);
        List<String> gndClasses = getGNDClassifications(record);
        return StringUtils.containsPrefix(gndClasses, classification);
    }

    /**
     * Gibt die Ländercodes.
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getCountryCodes(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfSubfields(record, "043");
    }

    public static boolean containsCountryCode(final Record record) {
        assertGNDRecord(record);
        return !getCountryCodes(record).isEmpty();
    }

    /**
     * Gibt die Redaktionellen Bemerkungen.
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getNonpublicGeneralNotes(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfFirstSubfields(record, "667", 'a');
    }

    /**
     * Gibt die Benutzungshinweise.
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getPublicGeneralNotes(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfFirstSubfields(record, "680", 'a');
    }

    /**
     * Gibt die Quellenangaben ohne erläuternde Texte und URIs.
     * 
     * @param record	nicht null.	 * 
     * @return			nicht null.
     */
    public static List<String> getSourcesDataFound(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfFirstSubfields(record, "670", 'a');
    }

    /**
     * Gibt die Quellenangaben.
     * 
     * @param record	nicht null.	 * 
     * @return			nicht null.
     */
    public static List<Line> getSourceLines(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getLines(record, "670");
    }

    public static boolean containsSource(final Record record) {
        assertGNDRecord(record);
        return !getSourceLines(record).isEmpty();
    }

    /**
     * Gibt die Negativ eingesehene Quellen.
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getSourceDataNotFound(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfSubfields(record, "675");
    }

    /**
     * Gibt die Definitionen.
     * @param record	nicht null.
     * 
     * @return			nicht null.
     */
    public static List<String> getDefinitions(final Record record) {
        assertGNDRecord(record);
        return RecordUtils.getContentsOfFirstSubfields(record, "679", 'a');
    }

    /**
     * Gibt die gnd-Idn (nid).
     * 
     * @param record 	nicht null
     * @return			Nummer oder null
     */
    public static String getSystemControlNumber(final Record record) {
        return RecordUtils.getContentOfSubfield(record, "035", '0');
    }

    /**
     * Gibt die gnd-Idn.
     * 
     * @param record 	nicht null
     * @return			Nummer oder null
     */
    public static String getNID(final Record record) {
        return getSystemControlNumber(record);
    }

    public static String toAleph(Subfield subfield, boolean explicit) {
        RangeCheckUtils.assertReferenceParamNotNull("subfield", subfield);
        String content = subfield.getContent();
        content = makeAlephStopword(content);
        return toAleph(subfield.getIndicator().alephChar, content,
            getSubfieldDescription(subfield.getIndicator()), explicit);
    }

    /**
     * Umgibt nichtsortierende Artikel am Anfang mit <<>>.
     * 
     * @param content
     * @return
     */
    public static String makeAlephStopword(String content) {
        if (content.contains(" @")) {
            content = "<<" + content.replace(" @", ">> ");
        }
        return content;
    }

    /**
     * 
     * @param indicator
     * @param subContent
     * @param subfieldDescription
     * @param explicit
     * @return
     */
    public static String toAleph(
        char indicator,
        String subContent,
        String subfieldDescription,
        boolean explicit) {
        String s;
        if (explicit)
            s = explicitSubfieldSep;
        else
            s = alephSubfieldSep;
        s += indicator;
        if (explicit)
            s += explicitIndicatorContentSep;
        else
            s += alephIndicatorContentSep;
        s += subContent;
        if (explicit)
            s += "\t\t" + subfieldDescription;
        return s;
    }

    public static String toAleph(
        Tag tag,
        Collection<Subfield> subfields,
        boolean explicit) {
        RangeCheckUtils.assertReferenceParamNotNull("tag", tag);
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        String s = toAlephPrefix(tag.aleph, getTagDescription(tag), explicit);
        for (Subfield subfield : subfields) {
            s += toAleph(subfield, explicit);
        }
        return s;
    }

    public static String toAleph(Line line, boolean explicit) {
        RangeCheckUtils.assertReferenceParamNotNull("line", line);
        return toAleph(line.getTag(), line.getSubfields(), explicit);
    }

    /**
     * Findet auf irgendeine Weise zu einer idn die 
     * GND-Nummer (übers WEB?).
     * 
     * @author baumann
     *
     */
    public static class GNDNumberFinder {
        public String find(String idn) {
            return idn;
        }
    }

    public static final String DOLLAR_9_PHRASE = "(DE-588)";

    private static GNDNumberFinder defaultFinder = new GNDNumberFinder();

    /**
     * 
     * Default-Aleph-Darstellung eines relationierten Feldes. Das Feld 
     * muss relationierte idn und Expansion enthalten.
     * @param line 		nicht null
     * @param finder 	wenn null, dann wird die idn als GND-Nummer verwendet.
     * @param explicit TODO
     * @return			Alephdarstellung oder leeren String.	
     */
    public static String toAlephRelationField(
        Line line,
        GNDNumberFinder finder,
        boolean explicit) {
        return toAlephRelationField(line.getTag(), line.getSubfields(), finder,
            explicit);
    }

    public static String getSubfieldDescription(Indicator indicator) {
        return "(" + indicator.descGerman + ")";
    }

    public static String getTagDescription(Tag tag) {
        return " --- " + tag.german;
    }

    /**
     * 
     * Default-Aleph-Darstellung eines relationierten Feldes. Das Feld 
     * muss relationierte idn und Expansion enthalten.
     * @param tag 		nicht null
     * @param subfields nicht null
     * @param finder 	wenn null, dann wird die idn als GND-Nummer verwendet.
     * @param explicit TODO
     * @return			Alephdarstellung oder leeren String.	
     */
    public static String toAlephRelationField(
        Tag tag,
        Collection<Subfield> subfields,
        GNDNumberFinder finder,
        boolean explicit) {
        RangeCheckUtils.assertReferenceParamNotNull("tag", tag);
        RangeCheckUtils.assertReferenceParamNotNull("subfields", subfields);
        Subfield dollar8 = SubfieldUtils.getFirstSubfield(subfields, '8');
        Subfield dollar9 = SubfieldUtils.getFirstSubfield(subfields, '9');
        if (dollar9 == null)
            return "";
        if (finder == null)
            finder = defaultFinder;
        List<Subfield> subList =
            SubfieldUtils.removeSubfieldsFromCollection(subfields, '8', '9');

        String s = toAlephPrefix(tag.aleph, getTagDescription(tag), explicit);
        char indChar = getAlephIndicator(tag.aleph);
        if (dollar8 != null) {
            String dollar8Content = dollar8.getContent();
            dollar8Content = dollar8Content.replace(" <", "\n\t$h ");
            dollar8Content = dollar8Content.replace(">", "");
            dollar8Content = makeAlephStopword(dollar8Content);
            s +=
                toAleph(indChar, dollar8Content,
                    getSubfieldDescription(TagDB.DOLLAR_8), explicit);
        }
        for (Subfield subfield : subList) {
            s += toAleph(subfield, explicit);
        }
        String relID = dollar9.getContent();
        String gndID = finder.find(relID);
        s +=
            toAleph('9', DOLLAR_9_PHRASE + gndID,
                getSubfieldDescription(TagDB.DOLLAR_9), explicit);
        return s;
    }

    /**
     * Liefert den Anfang einer Zeile im Aleph-Format.
     * 
     * 
     */
    private static String toAlephPrefix(
        String alephTag,
        String description,
        boolean explicit) {
        String s = alephTag;
        if (explicit)
            s += description;
        else
            s += alephTagSubfieldSep;
        return s;
    }

    /**
     * Gibt zu einem relationierten Tag den ersten Indikator.
     * 
     * @param alephTag	nicht null.
     * @return			früheren Indikator bei Relationen
     */
    public static char getAlephIndicator(String alephTag) {
        RangeCheckUtils.assertReferenceParamNotNull("alephTag", alephTag);
        char indChar;
        if (alephTag.equals("500"))
            indChar = 'p';
        else if (alephTag.equals("510"))
            indChar = 'k';
        else if (alephTag.equals("511"))
            indChar = 'e';
        else if (alephTag.equals("530"))
            indChar = 't';
        else if (alephTag.equals("550"))
            indChar = 's';
        else if (alephTag.equals("551"))
            indChar = 'g';
        else
            indChar = 'a';
        return indChar;
    }

    public static String toAleph(
        Record record,
        GNDNumberFinder finder,
        boolean explicit) {
        assertGNDRecord(record);
        if (record.tagDB != TAG_DB)
            throw new IllegalArgumentException("Kein GND-Datensatz");

        Line authorLine = WorkUtils.getAuthor(record);
        String authorName = null;
        if (authorLine != null) {
            authorName =
                SubfieldUtils.getContentOfFirstSubfield(authorLine, '8');
            if (authorName == null) // keine Expansion dabei
                authorName = "-Name nicht ermittelbar-";
        }

        List<String> lines = new LinkedList<String>();
        for (Line line : record) {
            Tag tag = line.getTag();
            String tagStr = tag.pica3;
            if ((tagStr.equals("130") || tagStr.equals("430"))
                && authorName != null) {
                // Sonderbehandlung für Werktitel:
                String newLine;
                newLine =
                    toAlephPrefix(tagStr.charAt(0) + "00",
                        getTagDescription(tag), explicit);
                newLine +=
                    toAleph('p', authorName, "(Autor/Komponist)", explicit);

                List<Subfield> subs = line.getSubfields();
                for (Subfield subfield : subs) {
                    newLine += toAleph(subfield, explicit);
                }
                lines.add(newLine);
                continue;
            }
            if (line.getTag().aleph != null) {
                if (SubfieldUtils.containsIndicator(line, '9')) {
                    lines.add(toAlephRelationField(line, finder, explicit));
                } else {
                    lines.add(toAleph(line, explicit));
                }
            } else {
                // Sonderbehandlung für einzelne Tags, die auf mehrere
                // Aleph-Tags aufgeteilt werden.
            }
        }
        StringUtils.tagSort(lines);
        return StringUtils.concatenate(lines);
    }

    /**
     * Liest einen Datensatz von der Standardeingabe. Nimmt an, dass Normdaten
     * vorliegen.
     * 
     * @return neuen Datensatz.
     */
    public static Record readFromConsole() {
        return RecordUtils.readFromConsole(TAG_DB);
    }

    /**
     * Liest einen Datensatz aus der Zwischenablage. Fehler werden
     * ignoriert.
     * @return		neuen Datensatz oder null.
     */
    public static Record readFromClip() {
        return RecordUtils.readFromClip(TAG_DB);
    }

    /**
    * @param args
    * @throws IllFormattedLineException 
    * @throws OperationNotSupportedException 
    * @throws IOException 
    */
    public static void main(String[] args)
        throws IllFormattedLineException,
        OperationNotSupportedException,
        IOException {
        Record record = RecordUtils.readFromClip();
        System.out.println(getAllDDCNumbersAndDet(record));
    }

    /**
    * @param args
    * @throws IllFormattedLineException 
    * @throws OperationNotSupportedException 
    * @throws IOException 
    */
    public static void main2(String[] args)
        throws IllFormattedLineException,
        OperationNotSupportedException,
        IOException {
        Line line =
            LineParser.parse("382 !123456779!Trompete$n2", TAG_DB, false);
        System.out.println(toAlephRelationField(line.getTag(),
            line.getSubfields(), null, true));
    }
}
